use core::slice;
use std::{
    alloc::{alloc, Layout},
    collections::HashMap,
};

use ab_glyph::{Font, FontRef, InvalidFont, OutlinedGlyph, PxScale};
use bliquez::{
    buffer::DeviceTexture2D,
    material::{attribute::Uniform, Attribute},
    mesh::MeshInstance,
    shader::Shader,
    Brick, Context, Material, Scene, Vertex,
};
use cgmath::{vec2, Matrix4, Vector2};
use dlv_list::{Index, VecList};
use mark_dirty::DirtyMark;
use winit::{
    dpi::PhysicalSize,
    event::{Event, WindowEvent},
    event_loop::{ControlFlow, EventLoop},
    keyboard::{Key, NamedKey},
    window::WindowBuilder,
};

pub fn emplace_array<T>(size: usize) -> Box<[T]> {
    unsafe {
        let ptr = alloc(Layout::array::<T>(size).unwrap()) as *mut T;
        let s = slice::from_raw_parts_mut(ptr, size);

        Box::from_raw(s)
    }
}

#[derive(Clone, Copy)]
pub struct GlyphData {
    pub ch: char,
    pub index: u8,
    pub x_min: i8,
    pub x_max: i8,
    pub y_min: i8,
    pub y_max: i8,
    pub advance: u8,
}

impl GlyphData {
    pub fn new(fc: &FontCache, ch: char, index: u8, outline: &OutlinedGlyph) -> Self {
        let x_min = outline.px_bounds().min.x as i8;
        let y_min = outline.px_bounds().min.y as i8;
        let x_max = outline.px_bounds().max.x as i8;
        let y_max = outline.px_bounds().max.y as i8;
        let advance = (fc.scale.x / fc.font.height_unscaled()
            * fc.font.h_advance_unscaled(outline.glyph().id)) as u8;
        Self {
            ch,
            index,
            x_min,
            x_max,
            y_min,
            y_max,
            advance,
        }
    }
}

pub struct FontCache<'font> {
    font: FontRef<'font>,
    scale: PxScale,
    baseline: u8,
    datas: VecList<GlyphData>,
    map: HashMap<char, Index<GlyphData>>,
    texture: DeviceTexture2D,
}

impl<'font> FontCache<'font> {
    pub fn new(
        ctx: &Context,
        font_data: &'font [u8],
        scale: f32,
        baseline: u8,
    ) -> Result<Self, InvalidFont> {
        Ok(Self {
            font: FontRef::try_from_slice(font_data)?,
            baseline,
            scale: PxScale::from(scale),
            datas: VecList::with_capacity(256),
            map: HashMap::with_capacity(256),
            texture: DeviceTexture2D::new(ctx, 256, 256, wgpu::TextureFormat::R8Uint),
        })
    }

    pub fn get_glyph(&mut self, ctx: &Context, ch: char) -> Option<GlyphData> {
        if let Some(idx) = self.map.get(&ch) {
            if *idx != self.datas.front_index().unwrap() {
                self.datas
                    .move_before(*idx, self.datas.front_index().unwrap());
            }
            Some(*self.datas.front().unwrap())
        } else {
            self.add_glyph(ctx, ch)
        }
    }

    pub fn baseline(&self) -> u8 {
        self.baseline
    }

    fn add_glyph(&mut self, ctx: &Context, ch: char) -> Option<GlyphData> {
        let index = if self.datas.len() >= self.datas.capacity() {
            let removed = self.datas.pop_back()?;
            self.map.remove(&removed.ch);
            removed.index
        } else {
            self.datas.len() as u8
        };

        // draw outline.
        let outline = self
            .font
            .outline_glyph(self.font.glyph_id(ch).with_scale(self.scale))?;
        let data = GlyphData::new(&self, ch, index, &outline);
        let mut buf = [0u8; 256];
        outline.draw(|x, y, o| {
            if o > 0.0 {
                buf[(y * 16 + x) as usize] = 1
            }
        });

        let index = index as u32;

        let col = index % 16;
        let row = index / 16;

        self.texture.write_pixels(
            ctx,
            col * 16,
            row * 16,
            16,
            16,
            &buf,
            wgpu::ImageDataLayout {
                offset: 0,
                bytes_per_row: Some(16),
                rows_per_image: Some(16),
            },
        );

        self.map.insert(ch, self.datas.push_front(data));

        Some(data)
    }
}

impl Attribute for FontCache<'static> {
    fn ty(&self) -> wgpu::BindingType {
        self.texture.ty()
    }

    fn res(&self) -> wgpu::BindingResource {
        self.texture.res()
    }

    fn prepare(&mut self, _: &Context) {}
}

#[derive(Clone, Copy)]
pub struct GlyphVertex {
    pub position: Vector2<f32>,
    pub uv: Vector2<f32>,
}

impl Vertex for GlyphVertex {
    const ATTRS: &'static [wgpu::VertexAttribute] =
        &wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2];
}

pub fn gv(x: f32, y: f32, u: f32, v: f32) -> GlyphVertex {
    GlyphVertex {
        position: vec2(x, y),
        uv: vec2(u, v),
    }
}

pub struct GlyphBatch {
    pipeline: wgpu::RenderPipeline,
    mesh: MeshInstance<GlyphVertex>,
    text: DirtyMark<String>,
    material: Material,
}

impl GlyphBatch {
    pub fn new(ctx: &Context, scene: &Scene, material: Material) -> Self {
        let mesh = MeshInstance::new(ctx);
        Self {
            pipeline: ctx.create_mesh_pipeline(
                &[scene.global_layout(0).unwrap(), material.layout()],
                &Shader::from_src::<GlyphVertex>(ctx, include_str!("text.wgsl")),
            ),
            text: DirtyMark::new(String::new()),
            mesh,
            material,
        }
    }

    pub fn set_text(&mut self, text: &str) {
        *self.text = text.into();
    }

    pub fn load_chars(&mut self, ctx: &Context) {
        self.mesh.clear();
        let cache = self.material.attr_mut::<FontCache<'static>>(0).unwrap();
        let mut start = -1.0;
        let len = self.text.chars().fold(0.0, |f, ch| {
            f + cache.get_glyph(ctx, ch).map(|g| g.advance).unwrap_or(8) as f32
        });
        let unit = 2.0 / len;
        for ch in self.text.chars() {
            if let Some(glyph) = cache.get_glyph(ctx, ch) {
                let height = (glyph.y_max - glyph.y_min) as f32;
                let width = (glyph.x_max - glyph.x_min) as f32;
                let advance = glyph.advance as f32 * unit;
                let x_min = glyph.x_min as f32 * unit;
                let x_max = glyph.x_max as f32 * unit;
                let y_min = glyph.y_min as f32 * unit * 2.0;
                let y_max = glyph.y_max as f32 * unit * 2.0;

                let left = start + x_min;
                let right = start + x_max;
                let bottom = -y_max;
                let top = -y_min;

                println!("ch: {ch} \nl:{left} | r: {right} | t: {top} | b: {bottom}");

                let origin_u = (glyph.index % 16 * 16) as f32;
                let origin_v = (glyph.index / 16 * 16) as f32;

                println!("u: {origin_u} | v: {origin_v} | w: {width} | h: {height}");

                self.mesh.add_quad([
                    gv(left, bottom, origin_u, origin_v + height),
                    gv(left, top, origin_u, origin_v),
                    gv(right, top, origin_u + width, origin_v),
                    gv(right, bottom, origin_u + width, origin_v + height),
                ]);

                start += advance;
            } else {
                start += 8.0 * unit;
            }
        }
    }
}

impl Brick for GlyphBatch {
    fn prepare(&mut self, ctx: &bliquez::Context) {
        if self.text.is_dirty() {
            self.load_chars(ctx);
            self.text.unmark_dirty();
        }

        self.mesh.prepare(ctx);
        self.material.prepare(ctx);
    }

    fn render<'pass>(&'pass self, pass: &mut wgpu::RenderPass<'pass>) {
        pass.set_pipeline(&self.pipeline);
        pass.set_bind_group(1, self.material.group(), &[]);
        self.mesh.render(pass);
    }
}

fn main() {
    run_app("Hello Text", |ctx| {
        let cache = FontCache::new(ctx, include_bytes!("unifont-15.1.04.otf"), 16.0, 14).unwrap();
        let toggle = Uniform::new(ctx, 0u32);
        let material = Material::builder()
            .attr(cache, wgpu::ShaderStages::FRAGMENT)
            .attr(toggle, wgpu::ShaderStages::FRAGMENT)
            .build(ctx);

        let mut scene = Scene::new(wgpu::Color::BLACK);
        scene.put_global(
            0,
            Material::builder()
                .attr(
                    Uniform::new(&ctx, Matrix4::from_scale(1.0f32)),
                    wgpu::ShaderStages::VERTEX,
                )
                .build(&ctx),
        );

        let mut g = GlyphBatch::new(ctx, &scene, material);
        g.set_text("Hello, World!");

        scene.add_brick("glyph", g);
        scene
    });
}

pub fn run_app(window_title: &str, create_scene: impl FnOnce(&Context) -> Scene) {
    let el = EventLoop::new().unwrap();
    let window = WindowBuilder::new()
        .with_title(window_title)
        .with_inner_size(PhysicalSize {
            width: 800,
            height: 400,
        })
        .build(&el)
        .unwrap();

    let mut ctx = Context::new(&window, 800, 800);

    let mut scene = create_scene(&ctx);

    let mut toggle = false;

    el.set_control_flow(ControlFlow::Poll);
    el.run(|event, target| match event {
        Event::WindowEvent {
            event: WindowEvent::CloseRequested,
            ..
        } => target.exit(),
        Event::AboutToWait => {
            window.request_redraw();
        }
        Event::WindowEvent {
            event: WindowEvent::KeyboardInput { event, .. },
            ..
        } => {
            if event.state.is_pressed() {
                match event.logical_key {
                    Key::Named(NamedKey::Space) => {
                        toggle = !toggle;
                        let text = scene.get_brick_mut::<GlyphBatch>("glyph").unwrap();
                        text.material
                            .attr_mut::<Uniform<u32>>(1)
                            .unwrap()
                            .set_data(if toggle { 1 } else { 0 });
                    }
                    _ => {}
                }
            }
        }
        Event::WindowEvent {
            event: WindowEvent::Resized(size),
            ..
        } => {
            ctx.resize(size.width, size.height);
        }
        Event::WindowEvent {
            event: WindowEvent::RedrawRequested,
            ..
        } => ctx.render_scene(&mut scene),
        _ => (),
    })
    .unwrap();
}
